"""
jackson.console

A command line tool for managing and interacting with Jackson servers.

No support for lighttpd.conf files!
eric.paniagua:
ok
there are three types of values the shell stores right now
settings for django (DJANGO_SETTINGS)
console environment variables (ENVIRONMENT)
and user defined variables (VARIABLES)
define IP MY_IP
show IP

when console.py starts up, it loads default values for DJANGO_SETTINGS from defaults.py
alternatively, an existing settings.py file can be loaded using "load django-settings <filename>"
"to write the current settings to a file, "save settings <filename>""
eric:"so you might start it up"
set MY_IP '127.0.0.1'
set MY_PORT 8080
set DATABASE_NAME 'chord1.db'
save settings settings1.py


(go start up lighttpd/django/fastcgi in two terminals like you've been doing, 
using the settings file)

init

* repeat in a separate console, with port 8081, chord2.db, etc *
but instead of init...
define first new server 127.0.0.1 8080 A

"""
from string import atoi
from subprocess import *
import sys, os
sys.path.append('../')

from jackson.chord import Server
from jackson.chord import utils

import jackson.extjson.ext_json as json
import urllib2, urllib, hashlib

# Define some utility functions
def base_url():
    global ENVIRONMENT
    return 'http://' + get_ip() + ':' + get_port() + '/' + ENVIRONMENT['URLJNK']

def confirm(message):
    ans = raw_input(message)
    if ans[0] in ['y', 'Y']:
        return True
    else:
        return False

def extract_arg(seq):
    global DJANGO_SETTINGS
    global ENVIRONMENT
    global VARIABLES
    value = 'useless default value'
    offset = 0
    # Handle special types of values
    if seq[0] == "new":
        # Handle creating objects
        if seq[1] == "server":
            value = make_server(seq[2], atoi(seq[3]), seq[4])
            offset = 5
        else:
            raise Exception, "Keyword new: unknown type '" + seq[1] + "'"
    elif seq[0] == "raw":
        offset = 2 + atoi(seq[1])
        value = ' '.join(seq[2 : offset])
    elif seq[0] == "valof":
        offset = 2
        if DJANGO_SETTINGS.has_key(seq[1]):
            value = DJANGO_SETTINGS[seq[1]]
        elif ENVIRONMENT.has_key(seq[1]):
            value = ENVIRONMENT[seq[1]]
        elif VARIABLES.has_key(seq[1]):
            value = VARIABLES[seq[1]]
        else:
            raise Exception, "Unknown name '" + seq[1] + "'"
    else:
        value = seq[0]
        offset = 1
        
    return value, seq[offset:]

def get_ip():
    global DJANGO_SETTINGS
    if DJANGO_SETTINGS.has_key('MY_IP'):
        return DJANGO_SETTINGS['MY_IP'][1:-1]
    else:
        return 'none'

def get_name():
    global DJANGO_SETTINGS
    if DJANGO_SETTINGS.has_key('INSTANCE_NAME'):
        return DJANGO_SETTINGS['INSTANCE_NAME'][1:-1]
    else:
        return 'none'

def get_port():
    global DJANGO_SETTINGS
    if DJANGO_SETTINGS.has_key('MY_PORT'):
        return DJANGO_SETTINGS['MY_PORT']
    else:
        return 'none'

def load_django_settings_from_file(file):
    global DJANGO_SETTINGS
    global DJANGO_SETTINGS_IMPORTS
    global ENVIRONMENT
    fail = False
    print "Loading Django settings from '" + file + "'...",
    sys.stdout.flush()
    try:
        infile = open(file, 'r')
    except Exception, e:
        print "failed\n\nCould not open file:", e
        return
    try:
        importing = False
        for line in infile:
            if line == '### Start imports\n':
                importing = True
            elif line == '### End imports\n':
                importing = False
            elif importing:
                DJANGO_SETTINGS_IMPORTS.append(line)
            else:
                var = line.strip().split(' = ')
                if (line.find('#') == -1) and (len(var) == 2):
                    DJANGO_SETTINGS[var[0]] = var[1]
        if importing:
            raise Exception, "Badly formatted settings file."
    except Exception, e:
        print "failed\n\nException:", e
        fail = True
    finally:
        infile.close()
    if not fail:
        print "done"
        if not (file == ENVIRONMENT['DJANGO_CONF']):
            ENVIRONMENT['DJANGO_CONF'] = file
            print "\nNOTE: Environment variable DJANGO_CONF has been updated."

def load_variables_from_file(file):
    global VARIABLES
    fail = False
    print "Loading variables from '" + file + "'...",
    sys.stdout.flush()
    try:
        infile = open(file, 'r')
    except Exception, e:
        print "failed\n\nCould not open file:", e
        return
    try:
        for line in infile:
            var = line.split(' <-=-> ')
            if not (len(var) == 2):
                continue
            if VARIABLES.has_key(var[0]) and (not confirm("\n  Overwrite existing variable '" + var[0] + "'? ")):
                continue
            VARIABLES[var[0]] = json.loads(var[1])
    except Exception, e:
        print "failed\n\nException:", e
        fail = True
    finally:
        infile.close()
    if not fail:
        print "done"

def make_server(ip, port, status):
    server = Server()
    server.ip = ip
    server.port = port
    server.key = utils.generate_server_key(ip, port)
    server.status = status
    return server

def md5_hash(txt):
    m = hashlib.md5()
    m.update(txt)
    return m.hexdigest()

def parse_cmd(line):
    seq = line.split(' ')
    args = []
    while len(seq) > 0:
        value, seq = extract_arg(seq)
        args.append(value)
    return args

def print_server(server):
    print "IP:    ", server.ip
    print "Port:  ", server.port
    print "Key:   ", server.key
    print "Status:", server.status

def print_cdn_server(s):
    print_server(s['me'])
    print "low:   ", s['low']
    print "high:  ", s['high']
    print "status:", s['status']

def save_settings_to_file(file):
    global DJANGO_SETTINGS
    global DJANGO_SETTINGS_IMPORTS
    fail = False
    print "Saving Django settings to '" + file + "'...",
    sys.stdout.flush()
    if os.path.exists(file) and (not confirm("\n  Overwrite existing file '" + file + "'? ")):
        print "Operation canceled by user"
        return
    try:
        outfile = open(file, 'w')
    except Exception, e:
        print "failed\n\nException:", e
        return
    try:
        outfile.write('### Start imports\n')
        for line in DJANGO_SETTINGS_IMPORTS:
            outfile.write(line)
        outfile.write('### End imports\n')
        names = DJANGO_SETTINGS.keys()
        names.sort()
        for name in names:
            outfile.write(name + ' = ' + DJANGO_SETTINGS[name] + '\n')
    except Exception, e:
        print "failed\n\nException:", e
        fail = True
    finally:
        outfile.close()
    if not fail:
        print "done"

def save_variables_to_file(file):
    global VARIABLES
    fail = False
    print "Saving variables to '" + file + "'...",
    sys.stdout.flush()
    if os.path.exists(file) and (not confirm("\n  Overwrite existing file '" + file + "'? ")):
        print "Operation canceled by user"
        return
    try:
        outfile = open(file, 'w')
    except Exception, e:
        print "failed\n\nCould not open file:", e
        return
    try:
        for name in VARIABLES:
            outfile.write(name + ' <-=-> ' + json.dumps(VARIABLES[name]) + '\n')
    except Exception, e:
        print "failed\n\nException:", e
        fail = True
    finally:
        outfile.close()
    if not fail:
        print "done"

# Set some variables and defaults
ENVIRONMENT = { 'DEFAULT_DJANGO_CONF' : 'defaults.py',
                'DJANGO_CONF' : 'defaults.py',
                'PROMPT' : '> ',
                'URLJNK' : '',
               }
DJANGO_SETTINGS = {}
DJANGO_SETTINGS_IMPORTS = []
TYPES = { 'server' : Server,
         }
URLS = { 'cdn.dump.contententry'    : 'cdn/admin/dump_content_entry',
         'cdn.init'                 : 'cdn/admin/init_net',
         'cdn.join'                 : 'cdn/admin/join_net',
         'cdn.leave'                : 'cdn/admin/leave_net',
         'cdn.whoami'               : 'cdn/admin/whoami',
         'cdn.get_url'              : 'cdn/client/get_url',
         'cdn.upload'               : 'cdn/client/upload',
         'chord.init'               : 'chord/admin/init_ring',
         'chord.join'               : 'chord/admin/join_ring',
         'chord.leave'              : 'chord/admin/leave_ring',
         'chord.whoami'             : 'chord/admin/whoami',
        }
VARIABLES = {}
QUIT_FLAG = False

# Define command functions
def define_command(args, flag=True):
    """Sets the value of a variable
Usage: define <var_name> <value>"""
    global VARIABLES
    if VARIABLES.has_key(args[1]):
        if not confirm("Overwrite existing variable '" + args[1] + "'? "):
            return
    VARIABLES[args[1]] = args[2]
    if flag:
        CMD['show'](args)

def dump_command(args):
    """Displays various interval values of the server
Usage: dump ContentEntry"""
    if not (len(args) == 2):
        print "Invalid syntax. See help for details."
        return
    if args[1] == "ContentEntry":
        print "Requesting dump of ContentEntry table...",
        sys.stdout.flush()
        try:
            f = urllib2.urlopen(base_url() + URLS['cdn.dump.contententry'])
        except Exception, e:
            print "failed\n\nException", e
            return
        print "done"
        print "\nResponse..."
        response = json.loads(f.read())
        for entry in response:
            print "\nkey:'" + str(entry[0]) + "'"
            print "filename: " + str(entry[1])
            print "mimetype: " + str(entry[2])
            print "url:'" + str(entry[3]) + "'"
            print "server:" + str(json.loads(entry[4]))
            print "status:'" + str(entry[5]) + "'"
            print "md5:'" + str(entry[6]) + "'"
            print "urgency:" + str(entry[7])
    else:
        print "Unrecognized keyword '" + args[1] + "'"

def export_command(args, flag=True):
    """Sets an environment variable
Usage: export <env_var> <value>"""
    global ENVIRONMENT
    if not ENVIRONMENT.has_key(args[1]):
        raise Exception, "There is no such environment variable as '" + args[1] + "'"
    ENVIRONMENT[args[1]] = args[2]
    if flag:
        CMD['show'](args)

def hash_command(args):
    """Returns the hash value of a given string
Usage: hash <string>"""
    if not (len(args) == 2):
        print "Invalid syntax. See help for details."
        return
    print md5_hash(args[1])

def help_command(args, flag=True):
    """Displays information on available commands
Usage: help
Usage: help <command>"""
    global CMD
    if len(args) == 1:
        commands = CMD.keys()
        commands.sort()
        print "Available commands:\n\n" + '\n'.join(commands) + "\n\nType help <command name> for more information."
    elif CMD.has_key(args[1]):
        print CMD[args[1]].__doc__

def init_command(args, flag=True):
    """Instructs the server to initialize a new ring
Usage: init chord
Usage: init jackson"""
    if not (len(args) == 2):
        print "Invalid syntax. See help for details."
        return
    if args[1] == "chord":
        print "Initializing chord server...",
        sys.stdout.flush()
        
        try:
            f = urllib2.urlopen(base_url() + URLS['chord.init'])
        except Exception, e:
            print "failed\n\nException:", e
            return
            
        print "done"
        print "\nResponse..."
        print_server(json.loads(f.read())['me'])
    elif args[1] == "cdn":
        print "Initializing jackson server...",
        sys.stdout.flush()
        
        try:
            f = urllib2.urlopen(base_url() + URLS['cdn.init'])
        except Exception, e:
            print "failed\n\nException:", e
            return
        
        print "done"
        print "\nResponse..."
        print_server(json.loads(f.read())['me'])
    else:
        print "Bad parameter '" + args[1] + "'"

def join_command(args, flag=True):
    """Instructs the server to join the ring hosting the specified server
Usage: join chord <server>
Usage: join cdn <server>"""
    if not (len(args) == 3):
        print "Invalid syntax. See help for details."
        return
    if args[1] == "chord":
        print "Attempting to join chord ring...",
        sys.stdout.flush()
        
        try:
            f = urllib2.urlopen(base_url() + URLS['chord.join'] + '@' + args[2].ip + ':' + str(args[2].port))
        except Exception, e:
            print "failed\n\nException:", e
            return
        
        print "done"
        print "\nResponse..."
        response = json.loads(f.read())
        if response.has_key('exception'):
            print "Exception:", response['exception']
        else:
            print "Predecessor..."
            print_server(response['predecessor'])
            print "\nMe..."
            print_server(response['me'])
            print "\nSuccessor..."
            print_server(response['successor'])
    elif args[1] == "cdn":
        print "Attempting to join cdn...",
        sys.stdout.flush()
        
        try:
            f = urllib2.urlopen(base_url() + URLS['cdn.join'] + '@' + args[2].ip + ':' + str(args[2].port))
        except Exception, e:
            print "failed\n\nException:", e
            return
        
        print "done"
        print "\nResponse..."
        response = json.loads(f.read())
        if response.has_key('exception'):
            print "Exception:", response['exception']
        else:
            print "Predecessor..."
            print_server(response['predecessor'])
            print "\nMe..."
            print_server(response['me'])
            print "\nSuccessor..."
            print_server(response['successor'])
    else:
        print "Bad parameter '" + args[1] + "'"

def leave_command(args, flag=True):
    """Instructs the server to leave its ring
Usage: leave chord
Usage: leave cdn"""
    if not (len(args) == 2):
        print "Invalid syntax. See help for details."
        return
    if args[1] == "chord":
        print "Attempting to leave chord ring...",
        sys.stdout.flush()
        
        try:
            f = urllib2.urlopen(base_url() + URLS['chord.leave'])
        except Exception, e:
            print "failed\n\nException:", e
            return
        
        print "done"
        print "\nResponse..."
        print f.read()
    elif args[1] == "cdn":
        print "Attempting to leave cdn...",
        sys.stdout.flush()
        
        try:
            f = urllib2.urlopen(base_url() + URLS['cdn.leave'])
        except Exception, e:
            print "failed\n\nException:", e
            return
        
        print "done"
        print "\nResponse..."
        response = json.loads(f.read())
        if response.has_key('exception'):
            print "Exception:", response['exception']
        else:
            print "Successful"
    else:
        print "Bad parameter '" + args[1] + "'"

def load_command(args, flag=True):
    """Loads Django settings, overwriting current ones
Usage: load defaults
Usage: load settings <path-to-settings>
Usage: load variables <path-to-variables>"""
    if args[1] == 'defaults':
        load_django_settings_from_file(ENVIRONMENT['DEFAULT_DJANGO_CONF'])
    elif args[1] == 'settings':
        if len(args) == 2:
            load_django_settings_from_file(ENVIRONMENT['DJANGO_CONF'])
        else:
            load_django_settings_from_file(args[2])
    elif args[1] == 'variables':
        load_variables_from_file(args[2])
    else:
        print "I don't know how to load '" + args[1] + "'"
        return

def lookup_command(args):
    """Performs a chord lookup for the given key, returns the successor of that key
Usage: lookup <key>"""
    print "Performing lookup..."
    sys.stdout.flush()
    try:
        f = urllib2.urlopen(base_url() + URLS['chord.lookup'])
    except Exception, e:
        print "failed\n\nException:", e
        return
    print "done"
    print "\nResponse"
    response = json.loads(f.read())
    if response.has_key('excpeption'):
        print "Exception:", response['exception']
    else:
        print "Successor:"
        print_server(response['successor'])


def map_command(args):
    """Traces out the chord ring by following successor pointers, prints results to screen
Usage: map"""
    servers = []

def poke_command(args, flag=True):
    """Checks to see if this server or a specified server is up
Usage: poke
Usage: poke <server>"""
    if len(args) == 1:
        server = make_server(DJANGO_SETTINGS['MY_IP'], DJANGO_SETTINGS['MY_PORT'], 'A')
    else:
        server = args[1]
    print "Poking " + str(server) + "...",
    sys.stdout.flush()
    try:
        f = urllib2.urlopen(base_url())
    except Exception, e:
        print "failed\n\nException:", e
        return
    print "done"
    print "\nResponse:"
    print f.read()

def query_command(args):
    """Queries the cdn to find the value associated with the given (human-readable) key
Usage: query <key>"""
    if not (len(args) == 2):
        print "Invalid syntax. See help for details."
        return
    print "Querying...",
    sys.stdout.flush()
    try:
        json_data = json.dumps(md5_hash(args[1]))
        f = urllib2.urlopen(base_url() + URLS['cdn.get_url'], urllib.urlencode({ 'data' : json_data }))
    except Exception, e:
        print "fail\n\nException", e
        return
    print "done"
    print '\nResponse...'
    response = json.loads(f.read())
    print response

def quit_command(args, flag=True):
    """Exits the console
Usage: quit"""
    global QUIT_FLAG
    QUIT_FLAG = True
    print "Goodbye!"

def save_command(args, flag=True):
    """Saves the current Django settings to disk
Usage: save settings <path-to-file>
Usage: save variables <path-to-file>"""
    if args[1] == 'settings':
        filename = ''
        if len(args) == 2:
            filename = ENVIRONMENT['DJANGO_CONF']
        else:
            filename = args[2]
        save_settings_to_file(filename)
    elif args[1] == 'variables':
        if not (len(args) == 3):
            print "You must specify a filename"
            return
        save_variables_to_file(args[2])
    else:
        print "I don't know how to save '" + args[1] + "'"

def set_command(args, flag=True):
    """Changes the value of a Django setting
Usage: set <setting_name> <value>"""
    global DJANGO_SETTINGS
    if not DJANGO_SETTINGS.has_key(args[1]):
        raise Exception, "Invalid Django setting '" + args[1] + "'"
    DJANGO_SETTINGS[args[1]] = args[2]
    if flag:
        CMD['show'](args)

def show_command(args, flag=True):
    """Displays the value of a specified setting, environment variable, variable, or group thereof
Usage: show <name>
Usage: show SETTINGS
Usage: show ENVIRONMENT
Usage: show VARIABLES"""
    global DJANGO_SETTINGS
    global ENVIRONMENT
    global VARIABLES
    if DJANGO_SETTINGS.has_key(args[1]):
        print args[1] + " = " + str(DJANGO_SETTINGS[args[1]])
    elif ENVIRONMENT.has_key(args[1]):
        print args[1] + " = " + str(ENVIRONMENT[args[1]])
    elif VARIABLES.has_key(args[1]):
        print args[1] + " = " + str(VARIABLES[args[1]])
    # Handle keywords...
    elif args[1] == "SETTINGS":
        # DJANGO_SETTINGS - print all settings along with their values, alphabetical order
        names = DJANGO_SETTINGS.keys()
        names.sort()
        for name in names:
            print name + " = " + str(DJANGO_SETTINGS[name])
    elif args[1] == "ENVIRONMENT":
        # ENVIRONMENT - print all environment variables along with their values, alphabetical order
        names = ENVIRONMENT.keys()
        names.sort()
        for name in names:
            print name + " = " + str(ENVIRONMENT[name])
    elif args[1] == "VARIABLES":
        # VARIABLES - print all settings along with their values, arbitrary order
        names = VARIABLES.keys()
        names.sort()
        for name in names:
            print name + " = " + str(VARIABLES[name])
    else:
        print "Invalid name or keyword '" + args[1] + "'"

def start_command(args, flag=True):
    """Currently a dud"""
    print "Please start the server in a separate terminal using:"
    print "python manage.py --settings=<your settings file here> runserver"

def start2_command(args, flag=True):
    """Starts a django server with the specified settings. Assumes a compatible lighttpd server is running.
"""
    command = 'python manage.py runfcgi daemonize=false --settings=' + ENVIRONMENT['DJANGO_CONF'][:-2]
    command = command + ' method=threaded host=' + DJANGO_CONF['FASTCGI_IP'] + ' port=' + DJANGO_CONF['FASTCGI_PORT']
    command = command + ' pidfile=' + DJANGO_CONF['INSTANCE_NAME']

def stop_command(args, flag=True):
    """Currently a dud"""
    print "Please stop the server in its separate terminal using Ctrl-C"

def upload_command(args):
    """Registers a key/value pair with the cdn network; key and value are strings
Usage: register <key> <value>"""
    if not (len(args) == 3):
        print "Invalid syntax. See help for details"
        return
    print "Registering key/value pair...",
    sys.stdout.flush()
    try:
        d = {}
        d['key'] = md5_hash(args[1])
        d['url'] = args[2]
        json_data = json.dumps(d)
        f = urllib2.urlopen(base_url() + URLS['cdn.upload'], urllib.urlencode({ 'data' : json_data }))
    except Exception, e:
        print "failed\n\nException:", e
        return
    print "done"
    print "\nResponse..."
    response = json.loads(f.read())
    for key in response:
        print key + ':', response[key]

def whoami_command(args):
    """Displays a Server object representing the server
Usage: whoami chord
Usage: whoami cdn"""
    if not (len(args) == 2):
        print "Invalid syntax. See help for details."
        return
    if args[1] == "chord":
        print "Asking chord server at " + DJANGO_SETTINGS['MY_IP'][1:-1] + ':' + DJANGO_SETTINGS['MY_PORT'] + "...",
        sys.stdout.flush()
        try:
            f = urllib2.urlopen(base_url() + ENVIRONMENT['URLJNK'] + URLS['chord.whoami'])
        except Exception, e:
            print "failed\n\nException:", e
            return
        print "done"
        print "\nResponse:"
        response = json.loads(f.read())
        if response.has_key('exception'):
            print "Exception:", response['exception']
        else:
            print "Predecessor..."
            print_server(response['predecessor'])
            print "\nMe..."
            print_server(response['me'])
            print "\nSuccessor..."
            print_server(response['successor'])
    elif args[1] == "cdn":
        print "Asking cdn server at " + DJANGO_SETTINGS['MY_IP'][1:-1] + ':' + DJANGO_SETTINGS['MY_PORT'] + "...",
        sys.stdout.flush()
        try:
            f = urllib2.urlopen(base_url() + ENVIRONMENT['URLJNK'] + URLS['cdn.whoami'])
        except Exception, e:
            print "failed\n\nException:", e
            return
        print "done"
        print "\nResponse:"
        response = json.loads(f.read())
        if response.has_key('exception'):
            print "Exception:", response['exception']
        else:
            print "Me..."
            print_cdn_server(response)
    else:
        print "Bad parameter '" + args[1] + "'"

# Define the commands
CMD = { 'define'    : define_command,
        'dump'      : dump_command,
        'export'    : export_command,
        'hash'      : hash_command,
        'help'      : help_command,
        'init'      : init_command,
        'join'      : join_command,
        'leave'     : leave_command,
        'load'      : load_command,
        'lookup'    : lookup_command,
        #'map'       : map_command,
        'poke'      : poke_command,
        'query'     : query_command,
        'quit'      : quit_command,
        'register'  : upload_command,
        'save'      : save_command,
        'set'       : set_command,
        'show'      : show_command,
        #'start'     : start_command,
        #'stop'      : stop_command,
        'whoami'    : whoami_command,
       }

if __name__ == "__main__":
    import sys
    
    print
    
    if len(sys.argv) >= 2:
        CMD['export'](['export', 'DJANGO_CONF', sys.argv[1]], False)
    
    # Initialize settings
    CMD['load'](['load', 'settings', ENVIRONMENT['DJANGO_CONF']])
    
    # Load variables
    if len(sys.argv) >= 3:
        CMD['load'](['load', 'variables', sys.argv[2]], False)
    
    print '\nWelcome to console!'
    
    # Main shell loop
    while not QUIT_FLAG:
        line = raw_input('\n' + get_name() + '@' + get_ip() + ':' + get_port() + ENVIRONMENT['PROMPT'])
        print
        try:
            args = parse_cmd(line)
        except Exception, e:
            print "Exception encountered parsing command '%s': %s" % (line, e)
            print "Check your syntax"
            continue
        if not CMD.has_key(args[0]):
            print "Sorry, I don't know how to %s: command not found" % args[0]
            continue
        try:
            CMD[args[0]](args)
        except Exception, e:
            print "Exception encountered interpreting command '%s': %s" % (args[0], e)
            continue
